allow exporting of a set of Agents with their links from a Scenario; Scenario guid is now generated and copied to export, as well as a source link when public

Andrew Cantino 10 years ago
parent
commit
663250227d

lib/assignable_types.rb → app/concerns/assignable_types.rb


lib/inheritance_tracking.rb → app/concerns/inheritance_tracking.rb


lib/json_serialized_field.rb → app/concerns/json_serialized_field.rb


lib/markdown_class_attributes.rb → app/concerns/markdown_class_attributes.rb


+ 15 - 2
app/controllers/scenarios_controller.rb

@@ -1,4 +1,6 @@
1 1
 class ScenariosController < ApplicationController
2
+  skip_before_filter :authenticate_user!, :only => :export
3
+
2 4
   def index
3 5
     @scenarios = current_user.scenarios.page(params[:page])
4 6
 
@@ -27,10 +29,8 @@ class ScenariosController < ApplicationController
27 29
     end
28 30
   end
29 31
 
30
-  # Share is a work in progress!
31 32
   def share
32 33
     @scenario = current_user.scenarios.find(params[:id])
33
-    @agents = @scenario.agents.preload(:scenarios).page(params[:page])
34 34
 
35 35
     respond_to do |format|
36 36
       format.html
@@ -38,6 +38,19 @@ class ScenariosController < ApplicationController
38 38
     end
39 39
   end
40 40
 
41
+  def export
42
+    @scenario = Scenario.find(params[:id])
43
+    raise ActiveRecord::RecordNotFound unless @scenario.public? || (current_user && current_user.id == @scenario.user_id)
44
+
45
+    @exporter = AgentsExporter.new(:name => @scenario.name,
46
+                                   :description => @scenario.description,
47
+                                   :guid => @scenario.guid,
48
+                                   :source_url => @scenario.public? && export_scenario_url(@scenario),
49
+                                   :agents => @scenario.agents)
50
+    response.headers['Content-Disposition'] = 'attachment; filename="' + @exporter.filename + '"'
51
+    render :json => JSON.pretty_generate(@exporter.as_json)
52
+  end
53
+
41 54
   def edit
42 55
     @scenario = current_user.scenarios.find(params[:id])
43 56
 

+ 7 - 1
app/models/scenario.rb

@@ -1,16 +1,22 @@
1 1
 class Scenario < ActiveRecord::Base
2
-  attr_accessible :name, :agent_ids
2
+  attr_accessible :name, :agent_ids, :description, :public
3 3
 
4 4
   belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios
5 5
   has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario
6 6
   has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios
7 7
 
8
+  before_save :make_guid
9
+
8 10
   validates_presence_of :name, :user
9 11
 
10 12
   validate :agents_are_owned
11 13
 
12 14
   protected
13 15
 
16
+  def make_guid
17
+    self.guid = SecureRandom.hex unless guid.present?
18
+  end
19
+
14 20
   def agents_are_owned
15 21
     errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user }
16 22
   end

+ 18 - 1
app/views/scenarios/_form.html.erb

@@ -12,12 +12,29 @@
12 12
     <div class="col-md-4">
13 13
       <div class="form-group">
14 14
         <%= f.label :name %>
15
-        <%= f.text_field :name, :class => 'form-control' %>
15
+        <%= f.text_field :name, :class => 'form-control', :placeholder => "Name your Scenario" %>
16 16
       </div>
17 17
     </div>
18 18
   </div>
19 19
 
20 20
   <div class="row">
21
+    <div class="col-md-8">
22
+      <div class="form-group">
23
+        <%= f.label :description, "Optional Description" %>
24
+        <%= f.text_area :description, :rows => 10, :class => 'form-control', :placeholder => "Optionally describe what this set of Agents will do" %>
25
+      </div>
26
+
27
+      <div class="checkbox">
28
+        <%= f.label :public do %>
29
+          <%= f.check_box :public %> Share this Scenario publicly
30
+        <% end %>
31
+        <span class="glyphicon glyphicon-question-sign hover-help" data-content="When selected, this Scenario and all Agents in it will be made public.  An export URL will be available to share with other Huginn users.  Be very careful that you do not have secret credentials stored in these Agents' options.  Instead, use Credentials by reference."></span>
32
+      </div>
33
+
34
+    </div>
35
+  </div>
36
+
37
+  <div class="row">
21 38
     <div class="col-md-4">
22 39
       <div class="form-group">
23 40
         <div>

+ 4 - 2
app/views/scenarios/index.html.erb

@@ -20,14 +20,16 @@
20 20
 
21 21
         <% @scenarios.each do |scenario| %>
22 22
           <tr>
23
-            <td><span class='label label-info'><%= scenario.name %></span></td>
23
+            <td>
24
+              <%= link_to(scenario.name, scenario, class: "label label-info") %>
25
+            </td>
24 26
             <td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td>
25 27
             <td>
26 28
               <div class="btn-group btn-group-xs" style="float: right">
27 29
                 <%= link_to 'Show', scenario, class: "btn btn-default" %>
28 30
                 <%= link_to 'Edit', edit_scenario_path(scenario), class: "btn btn-default" %>
29 31
                 <%= link_to 'Share', share_scenario_path(scenario), class: "btn btn-default" %>
30
-                <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-default" %>
32
+                <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: { confirm: "This will remove the '#{scenario.name}' Scenerio from all Agents and delete it.  Are you sure?" }, class: "btn btn-default" %>
31 33
               </div>
32 34
             </td>
33 35
           </tr>

+ 16 - 0
app/views/scenarios/share.html.erb

@@ -5,6 +5,22 @@
5 5
         <h2>Share <span class='label label-info scenario'><%= @scenario.name %></span> with the world</h2>
6 6
       </div>
7 7
 
8
+      <p>
9
+        <strong>Please be sure that none of the Agents in this Scenario have sensitive data in their settings before sharing!</strong>
10
+      </p>
11
+
12
+      <% if @scenario.public? %>
13
+        <p>
14
+          This Scenario is public.  You can <%= link_to "download and share your export file", export_scenario_path(@scenario) %>, or give out this URL:
15
+        </p>
16
+
17
+        <form onsubmit='return false;'>
18
+          <input type='text' class='form-control' value='<%= export_scenario_url(@scenario) %>' onclick="return this.select();"/>
19
+        </form>
20
+      <% else %>
21
+        This Scenario is not public.  You can share it by <%= link_to "downloading and sharing your export file", export_scenario_path(@scenario) %>.
22
+      <% end %>
23
+
8 24
       <hr>
9 25
 
10 26
       <div class="row">

+ 6 - 7
app/views/scenarios/show.html.erb

@@ -2,20 +2,19 @@
2 2
   <div class='row'>
3 3
     <div class='col-md-12'>
4 4
       <div class="page-header">
5
-        <h2>Scenario <span class='label label-info scenario'><%= @scenario.name %></span></h2>
5
+        <h2><%= "Public" if @scenario.public? %> Scenario <span class='label label-info scenario'><%= @scenario.name %></span></h2>
6 6
       </div>
7 7
 
8
+      <%= render 'agents/table', :returnTo => scenario_path(@scenario) %>
9
+
10
+      <br/>
11
+
8 12
       <div class="btn-group">
9 13
         <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
10 14
         <%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %>
11 15
         <%= link_to '<span class="glyphicon glyphicon-share-alt"></span> Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %>
16
+        <%= link_to '<span class="glyphicon glyphicon-trash"></span> Delete'.html_safe, scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it.  Are you sure?" }, class: "btn btn-default" %>
12 17
       </div>
13
-
14
-      <div class="page-header">
15
-        <h3>Agents</h3>
16
-      </div>
17
-
18
-      <%= render 'agents/table', :returnTo => scenario_path(@scenario) %>
19 18
     </div>
20 19
   </div>
21 20
 </div>

+ 1 - 0
config/routes.rb

@@ -29,6 +29,7 @@ Huginn::Application.routes.draw do
29 29
   resources :scenarios do
30 30
     member do
31 31
       get :share
32
+      get :export
32 33
     end
33 34
   end
34 35
 

+ 8 - 0
db/migrate/20140531232016_add_fields_to_scenarios.rb

@@ -0,0 +1,8 @@
1
+class AddFieldsToScenarios < ActiveRecord::Migration
2
+  def change
3
+    add_column :scenarios, :description, :text
4
+    add_column :scenarios, :public, :boolean, :default => false, :null => false
5
+    add_column :scenarios, :guid, :string, :null => false
6
+    add_column :scenarios, :source_url, :string
7
+  end
8
+end

+ 73 - 69
db/schema.rb

@@ -9,23 +9,23 @@
9 9
 # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 10
 # you'll amass, the slower it'll run and the greater likelihood for issues).
11 11
 #
12
-# It's strongly recommended to check this file into your version control system.
12
+# It's strongly recommended that you check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(:version => 20140408150825) do
14
+ActiveRecord::Schema.define(version: 20140531232016) do
15 15
 
16
-  create_table "agent_logs", :force => true do |t|
17
-    t.integer  "agent_id",                         :null => false
18
-    t.text     "message",                          :null => false
19
-    t.integer  "level",             :default => 3, :null => false
16
+  create_table "agent_logs", force: true do |t|
17
+    t.integer  "agent_id",                                       null: false
18
+    t.text     "message",           limit: 16777215,             null: false
19
+    t.integer  "level",                              default: 3, null: false
20 20
     t.integer  "inbound_event_id"
21 21
     t.integer  "outbound_event_id"
22
-    t.datetime "created_at",                       :null => false
23
-    t.datetime "updated_at",                       :null => false
22
+    t.datetime "created_at",                                     null: false
23
+    t.datetime "updated_at",                                     null: false
24 24
   end
25 25
 
26
-  create_table "agents", :force => true do |t|
26
+  create_table "agents", force: true do |t|
27 27
     t.integer  "user_id"
28
-    t.text     "options"
28
+    t.text     "options",               limit: 16777215
29 29
     t.string   "type"
30 30
     t.string   "name"
31 31
     t.string   "schedule"
@@ -33,73 +33,62 @@ ActiveRecord::Schema.define(:version => 20140408150825) do
33 33
     t.datetime "last_check_at"
34 34
     t.datetime "last_receive_at"
35 35
     t.integer  "last_checked_event_id"
36
-    t.datetime "created_at",                                                     :null => false
37
-    t.datetime "updated_at",                                                     :null => false
38
-    t.text     "memory",                :limit => 2147483647
36
+    t.datetime "created_at",                                               null: false
37
+    t.datetime "updated_at",                                               null: false
38
+    t.text     "memory",                limit: 2147483647
39 39
     t.datetime "last_web_request_at"
40
-    t.integer  "keep_events_for",                             :default => 0,     :null => false
41 40
     t.datetime "last_event_at"
42 41
     t.datetime "last_error_log_at"
43
-    t.boolean  "propagate_immediately",                       :default => false, :null => false
44
-    t.boolean  "disabled",                                    :default => false, :null => false
42
+    t.integer  "keep_events_for",                          default: 0,     null: false
43
+    t.boolean  "propagate_immediately",                    default: false, null: false
44
+    t.boolean  "disabled",                                 default: false, null: false
45 45
   end
46 46
 
47
-  add_index "agents", ["schedule"], :name => "index_agents_on_schedule"
48
-  add_index "agents", ["type"], :name => "index_agents_on_type"
49
-  add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at"
47
+  add_index "agents", ["schedule"], name: "index_agents_on_schedule", using: :btree
48
+  add_index "agents", ["type"], name: "index_agents_on_type", using: :btree
49
+  add_index "agents", ["user_id", "created_at"], name: "index_agents_on_user_id_and_created_at", using: :btree
50 50
 
51
-  create_table "delayed_jobs", :force => true do |t|
52
-    t.integer  "priority",                       :default => 0
53
-    t.integer  "attempts",                       :default => 0
54
-    t.text     "handler",    :limit => 16777215
55
-    t.text     "last_error"
51
+  create_table "delayed_jobs", force: true do |t|
52
+    t.integer  "priority",                    default: 0
53
+    t.integer  "attempts",                    default: 0
54
+    t.text     "handler",    limit: 16777215
55
+    t.text     "last_error", limit: 16777215
56 56
     t.datetime "run_at"
57 57
     t.datetime "locked_at"
58 58
     t.datetime "failed_at"
59 59
     t.string   "locked_by"
60 60
     t.string   "queue"
61
-    t.datetime "created_at",                                    :null => false
62
-    t.datetime "updated_at",                                    :null => false
61
+    t.datetime "created_at",                              null: false
62
+    t.datetime "updated_at",                              null: false
63 63
   end
64 64
 
65
-  add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
65
+  add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree
66 66
 
67
-  create_table "events", :force => true do |t|
67
+  create_table "events", force: true do |t|
68 68
     t.integer  "user_id"
69 69
     t.integer  "agent_id"
70
-    t.decimal  "lat",                            :precision => 15, :scale => 10
71
-    t.decimal  "lng",                            :precision => 15, :scale => 10
72
-    t.text     "payload",    :limit => 16777215
73
-    t.datetime "created_at",                                                     :null => false
74
-    t.datetime "updated_at",                                                     :null => false
70
+    t.decimal  "lat",                           precision: 15, scale: 10
71
+    t.decimal  "lng",                           precision: 15, scale: 10
72
+    t.text     "payload",    limit: 2147483647
73
+    t.datetime "created_at",                                              null: false
74
+    t.datetime "updated_at",                                              null: false
75 75
     t.datetime "expires_at"
76 76
   end
77 77
 
78
-  add_index "events", ["agent_id", "created_at"], :name => "index_events_on_agent_id_and_created_at"
79
-  add_index "events", ["expires_at"], :name => "index_events_on_expires_at"
80
-  add_index "events", ["user_id", "created_at"], :name => "index_events_on_user_id_and_created_at"
78
+  add_index "events", ["agent_id", "created_at"], name: "index_events_on_agent_id_and_created_at", using: :btree
79
+  add_index "events", ["expires_at"], name: "index_events_on_expires_at", using: :btree
80
+  add_index "events", ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at", using: :btree
81 81
 
82
-  create_table "links", :force => true do |t|
82
+  create_table "links", force: true do |t|
83 83
     t.integer  "source_id"
84 84
     t.integer  "receiver_id"
85
-    t.datetime "created_at",                          :null => false
86
-    t.datetime "updated_at",                          :null => false
87
-    t.integer  "event_id_at_creation", :default => 0, :null => false
85
+    t.datetime "created_at",                       null: false
86
+    t.datetime "updated_at",                       null: false
87
+    t.integer  "event_id_at_creation", default: 0, null: false
88 88
   end
89 89
 
90
-  add_index "links", ["receiver_id", "source_id"], :name => "index_links_on_receiver_id_and_source_id"
91
-  add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id"
92
-
93
-  create_table "user_credentials", :force => true do |t|
94
-    t.integer  "user_id",                              :null => false
95
-    t.string   "credential_name",                      :null => false
96
-    t.text     "credential_value",                     :null => false
97
-    t.datetime "created_at",                           :null => false
98
-    t.datetime "updated_at",                           :null => false
99
-    t.string   "mode",             :default => "text", :null => false
100
-  end
101
-
102
-  add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true
90
+  add_index "links", ["receiver_id", "source_id"], name: "index_links_on_receiver_id_and_source_id", using: :btree
91
+  add_index "links", ["source_id", "receiver_id"], name: "index_links_on_source_id_and_receiver_id", using: :btree
103 92
 
104 93
   create_table "scenario_memberships", force: true do |t|
105 94
     t.integer  "agent_id",    null: false
@@ -109,37 +98,52 @@ ActiveRecord::Schema.define(:version => 20140408150825) do
109 98
   end
110 99
 
111 100
   create_table "scenarios", force: true do |t|
112
-    t.string   "name",       null: false
113
-    t.integer  "user_id",    null: false
101
+    t.string   "name",                        null: false
102
+    t.integer  "user_id",                     null: false
114 103
     t.datetime "created_at"
115 104
     t.datetime "updated_at"
105
+    t.text     "description"
106
+    t.boolean  "public",      default: false, null: false
107
+    t.string   "guid",                        null: false
108
+    t.string   "source_url"
116 109
   end
117 110
 
118
-  create_table "users", :force => true do |t|
119
-    t.string   "email",                  :default => "",    :null => false
120
-    t.string   "encrypted_password",     :default => "",    :null => false
111
+  create_table "user_credentials", force: true do |t|
112
+    t.integer  "user_id",                           null: false
113
+    t.string   "credential_name",                   null: false
114
+    t.text     "credential_value",                  null: false
115
+    t.datetime "created_at",                        null: false
116
+    t.datetime "updated_at",                        null: false
117
+    t.string   "mode",             default: "text", null: false
118
+  end
119
+
120
+  add_index "user_credentials", ["user_id", "credential_name"], name: "index_user_credentials_on_user_id_and_credential_name", unique: true, using: :btree
121
+
122
+  create_table "users", force: true do |t|
123
+    t.string   "email",                  default: "",    null: false
124
+    t.string   "encrypted_password",     default: "",    null: false
121 125
     t.string   "reset_password_token"
122 126
     t.datetime "reset_password_sent_at"
123 127
     t.datetime "remember_created_at"
124
-    t.integer  "sign_in_count",          :default => 0
128
+    t.integer  "sign_in_count",          default: 0
125 129
     t.datetime "current_sign_in_at"
126 130
     t.datetime "last_sign_in_at"
127 131
     t.string   "current_sign_in_ip"
128 132
     t.string   "last_sign_in_ip"
129
-    t.datetime "created_at",                                :null => false
130
-    t.datetime "updated_at",                                :null => false
131
-    t.boolean  "admin",                  :default => false, :null => false
132
-    t.integer  "failed_attempts",        :default => 0
133
+    t.datetime "created_at",                             null: false
134
+    t.datetime "updated_at",                             null: false
135
+    t.boolean  "admin",                  default: false, null: false
136
+    t.integer  "failed_attempts",        default: 0
133 137
     t.string   "unlock_token"
134 138
     t.datetime "locked_at"
135
-    t.string   "username",                                  :null => false
136
-    t.string   "invitation_code",                           :null => false
139
+    t.string   "username",                               null: false
140
+    t.string   "invitation_code",                        null: false
137 141
     t.integer  "scenario_count",         default: 0,     null: false
138 142
   end
139 143
 
140
-  add_index "users", ["email"], :name => "index_users_on_email", :unique => true
141
-  add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
142
-  add_index "users", ["unlock_token"], :name => "index_users_on_unlock_token", :unique => true
143
-  add_index "users", ["username"], :name => "index_users_on_username", :unique => true
144
+  add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
145
+  add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
146
+  add_index "users", ["unlock_token"], name: "index_users_on_unlock_token", unique: true, using: :btree
147
+  add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree
144 148
 
145 149
 end

+ 53 - 0
lib/agents_exporter.rb

@@ -0,0 +1,53 @@
1
+class AgentsExporter
2
+  attr_accessor :options
3
+
4
+  def initialize(options)
5
+    self.options = options
6
+  end
7
+
8
+  # Filename should have no commas or special characters to support Content-Disposition on older browsers.
9
+  def filename
10
+    ((options[:name] || '').downcase.gsub(/[^a-z0-9_-]/, '-').gsub(/-+/, '-').gsub(/^-|-$/, '').presence || 'exported-agents') + ".json"
11
+  end
12
+
13
+  def as_json(opts = {})
14
+    {
15
+      :name => options[:name].presence || 'No name provided',
16
+      :description => options[:description].presence || 'No description provided',
17
+      :source_url => options[:source_url],
18
+      :guid => options[:guid],
19
+      :exported_at => Time.now.utc.iso8601,
20
+      :agents => agents.map { |agent| agent_as_json(agent) },
21
+      :links => links
22
+    }
23
+  end
24
+
25
+  def agents
26
+    options[:agents].to_a
27
+  end
28
+
29
+  def links
30
+    agent_ids = agents.map(&:id)
31
+
32
+    contained_links = agents.map.with_index do |agent, index|
33
+      agent.links_as_source.where(:receiver_id => agent_ids).map do |link|
34
+        { :source => index, :receiver => agent_ids.index(link.receiver_id) }
35
+      end
36
+    end
37
+
38
+    contained_links.flatten.compact
39
+  end
40
+
41
+  def agent_as_json(agent)
42
+    {
43
+      :type => agent.type,
44
+      :name => agent.name,
45
+      :schedule => agent.schedule,
46
+      :keep_events_for => agent.keep_events_for,
47
+      :propagate_immediately => agent.propagate_immediately,
48
+      :disabled => agent.disabled,
49
+      :source_system_agent_id => agent.id,
50
+      :options => agent.options
51
+    }
52
+  end
53
+end

spec/lib/inheritance_tracking_spec.rb → spec/concerns/inheritance_tracking_spec.rb


+ 55 - 1
spec/controllers/scenarios_controller_spec.rb

@@ -32,6 +32,59 @@ describe ScenariosController do
32 32
     end
33 33
   end
34 34
 
35
+  describe "GET share" do
36
+    it "only displays Scenario share information for the current user" do
37
+      get :share, :id => scenarios(:bob_weather).to_param
38
+      assigns(:scenario).should eq(scenarios(:bob_weather))
39
+
40
+      lambda {
41
+        get :share, :id => scenarios(:jane_weather).to_param
42
+      }.should raise_error(ActiveRecord::RecordNotFound)
43
+    end
44
+  end
45
+
46
+  describe "GET export" do
47
+    it "returns a JSON file download from an instantiated AgentsExporter" do
48
+      get :export, :id => scenarios(:bob_weather).to_param
49
+      assigns(:exporter).options[:name].should == scenarios(:bob_weather).name
50
+      assigns(:exporter).options[:description].should == scenarios(:bob_weather).description
51
+      assigns(:exporter).options[:agents].should == scenarios(:bob_weather).agents
52
+      assigns(:exporter).options[:guid].should == scenarios(:bob_weather).guid
53
+      assigns(:exporter).options[:source_url].should be_false
54
+      response.headers['Content-Disposition'].should == 'attachment; filename="bob-s-weather-alert-scenario.json"'
55
+      response.headers['Content-Type'].should == 'application/json; charset=utf-8'
56
+      JSON.parse(response.body)["name"].should == scenarios(:bob_weather).name
57
+    end
58
+
59
+    it "only exports private Scenarios for the current user" do
60
+      get :export, :id => scenarios(:bob_weather).to_param
61
+      assigns(:scenario).should eq(scenarios(:bob_weather))
62
+
63
+      lambda {
64
+        get :export, :id => scenarios(:jane_weather).to_param
65
+      }.should raise_error(ActiveRecord::RecordNotFound)
66
+    end
67
+
68
+    describe "public exports" do
69
+      before do
70
+        scenarios(:jane_weather).update_attribute :public, true
71
+      end
72
+
73
+      it "exports public scenarios for other users when logged in" do
74
+        get :export, :id => scenarios(:jane_weather).to_param
75
+        assigns(:scenario).should eq(scenarios(:jane_weather))
76
+        assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather))
77
+      end
78
+
79
+      it "exports public scenarios for other users when logged out" do
80
+        sign_out :user
81
+        get :export, :id => scenarios(:jane_weather).to_param
82
+        assigns(:scenario).should eq(scenarios(:jane_weather))
83
+        assigns(:exporter).options[:source_url].should == export_scenario_url(scenarios(:jane_weather))
84
+      end
85
+    end
86
+  end
87
+
35 88
   describe "GET edit" do
36 89
     it "only shows Scenarios for the current user" do
37 90
       get :edit, :id => scenarios(:bob_weather).to_param
@@ -67,9 +120,10 @@ describe ScenariosController do
67 120
 
68 121
   describe "PUT update" do
69 122
     it "updates attributes on Scenarios for the current user" do
70
-      post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name" }
123
+      post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name", :public => "1" }
71 124
       response.should redirect_to(scenario_path(scenarios(:bob_weather)))
72 125
       scenarios(:bob_weather).reload.name.should == "new_name"
126
+      scenarios(:bob_weather).should be_public
73 127
 
74 128
       lambda {
75 129
         post :update, :id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" }

+ 6 - 0
spec/fixtures/scenarios.yml

@@ -1,7 +1,13 @@
1 1
 jane_weather:
2 2
   name: Jane's weather alert Scenario
3 3
   user: jane
4
+  description: Jane's weather alert system
5
+  public: false
6
+  guid: random-guid-generated-by-bob
4 7
 
5 8
 bob_weather:
6 9
   name: Bob's weather alert Scenario
7 10
   user: bob
11
+  description: Bob's weather alert system
12
+  public: false
13
+  guid: random-guid-generated-by-jane

+ 58 - 0
spec/lib/agents_exporter_spec.rb

@@ -0,0 +1,58 @@
1
+# encoding: utf-8
2
+
3
+require 'spec_helper'
4
+
5
+describe AgentsExporter do
6
+  describe "#as_json" do
7
+    let(:name) { "My set of Agents" }
8
+    let(:description) { "These Agents work together nicely!" }
9
+    let(:guid) { "some-guid" }
10
+    let(:source_url) { "http://yourhuginn.com/scenarios/2/export.json" }
11
+    let(:agent_list) { [agents(:jane_weather_agent), agents(:jane_rain_notifier_agent)] }
12
+    let(:exporter) { AgentsExporter.new(:agents => agent_list, :name => name, :description => description, :source_url => source_url, :guid => guid) }
13
+
14
+    it "outputs a structure containing name, description, the date, all agents & their links" do
15
+      data = exporter.as_json
16
+      data[:name].should == name
17
+      data[:description].should == description
18
+      data[:source_url].should == source_url
19
+      data[:guid].should == guid
20
+      Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc)
21
+      data[:links].should == [{ :source => 0, :receiver => 1 }]
22
+      data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) }
23
+      data[:agents].all? { |agent_json| agent_json[:source_system_agent_id] && agent_json[:type] && agent_json[:name] }.should be_true
24
+    end
25
+
26
+    it "does not output links to other agents" do
27
+      Link.create!(:source_id => agents(:jane_weather_agent).id, :receiver_id => agents(:jane_website_agent).id)
28
+      Link.create!(:source_id => agents(:jane_website_agent).id, :receiver_id => agents(:jane_rain_notifier_agent).id)
29
+
30
+      exporter.as_json[:links].should == [{ :source => 0, :receiver => 1 }]
31
+    end
32
+  end
33
+
34
+  describe "#filename" do
35
+    it "strips special characters" do
36
+      AgentsExporter.new(:name => "ƏfooƐƕƺbar").filename.should == "foo-bar.json"
37
+    end
38
+
39
+    it "strips punctuation" do
40
+      AgentsExporter.new(:name => "foo,bar").filename.should == "foo-bar.json"
41
+    end
42
+
43
+    it "strips leading and trailing dashes" do
44
+      AgentsExporter.new(:name => ",foo,").filename.should == "foo.json"
45
+    end
46
+
47
+    it "has a default when options[:name] is nil" do
48
+      AgentsExporter.new(:name => nil).filename.should == "exported-agents.json"
49
+    end
50
+
51
+    it "has a default when the result is empty" do
52
+      AgentsExporter.new(:name => "").filename.should == "exported-agents.json"
53
+      AgentsExporter.new(:name => "Ə").filename.should == "exported-agents.json"
54
+      AgentsExporter.new(:name => "-").filename.should == "exported-agents.json"
55
+      AgentsExporter.new(:name => ",,").filename.should == "exported-agents.json"
56
+    end
57
+  end
58
+end

+ 11 - 0
spec/models/scenario_spec.rb

@@ -26,6 +26,17 @@ describe Scenario do
26 26
     end
27 27
   end
28 28
 
29
+  describe "guid" do
30
+    it "gets created before_save, but only if it's not present" do
31
+      scenario = users(:bob).scenarios.new(:name => "some scenario")
32
+      scenario.guid.should be_nil
33
+      scenario.save!
34
+      scenario.guid.should_not be_nil
35
+
36
+      lambda { scenario.save! }.should_not change { scenario.reload.guid }
37
+    end
38
+  end
39
+
29 40
   describe "counters" do
30 41
     before do
31 42
       @scenario = users(:bob).scenarios.new(:name => "some scenario")